package lucee.runtime.instrumentation.unix;

import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketImpl;
import java.net.SocketOptions;

import lucee.commons.lang.ExceptionUtil;

/**
 * The Java-part of the {@link UNIXSocket} implementation.
 */
class UNIXSocketImpl extends SocketImpl {
	private static final int SHUT_RD = 0;
	private static final int SHUT_WR = 1;
	private static final int SHUT_RD_WR = 2;

	private String socketFile;
	private boolean closed = false;
	private boolean bound = false;
	private boolean connected = false;

	private boolean closedInputStream = false;
	private boolean closedOutputStream = false;

	private final AFUNIXInputStream in = new AFUNIXInputStream();
	private final AFUNIXOutputStream out = new AFUNIXOutputStream();

	UNIXSocketImpl() {
		super();
		this.fd = new FileDescriptor();
	}

	FileDescriptor getFD() {
		return fd;
	}

	@Override
	protected void accept(SocketImpl socket) throws IOException {
		final UNIXSocketImpl si = (UNIXSocketImpl) socket;
		NativeUnixSocket.accept(socketFile, fd, si.fd);
		si.socketFile = socketFile;
		si.connected = true;
	}

	@Override
	protected int available() throws IOException {
		return NativeUnixSocket.available(fd);
	}

	protected void bind(SocketAddress addr) throws IOException {
		bind(0, addr);
	}

	protected void bind(int backlog, SocketAddress addr) throws IOException {
		if (!(addr instanceof UNIXSocketAddress)) {
			throw new SocketException("Cannot bind to this type of address: " + addr.getClass());
		}
		final UNIXSocketAddress socketAddress = (UNIXSocketAddress) addr;
		socketFile = socketAddress.getSocketFile();
		NativeUnixSocket.bind(socketFile, fd, backlog);
		bound = true;
		this.localport = socketAddress.getPort();
	}

	@Override
	@SuppressWarnings("hiding")
	protected void bind(InetAddress host, int port) throws IOException {
		throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
	}

	private void checkClose() {
		if (closedInputStream && closedOutputStream) {
			// close();
		}
	}

	@Override
	protected synchronized void close() throws IOException {
		if (closed) {
			return;
		}
		closed = true;
		if (fd.valid()) {
			NativeUnixSocket.shutdown(fd, SHUT_RD_WR);
			NativeUnixSocket.close(fd);
		}
		if (bound) {
			NativeUnixSocket.unlink(socketFile);
		}
		connected = false;
	}

	@Override
	@SuppressWarnings("hiding")
	protected void connect(String host, int port) throws IOException {
		throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
	}

	@Override
	@SuppressWarnings("hiding")
	protected void connect(InetAddress address, int port) throws IOException {
		throw new SocketException("Cannot bind to this type of address: " + InetAddress.class);
	}

	@Override
	protected void connect(SocketAddress addr, int timeout) throws IOException {
		if (!(addr instanceof UNIXSocketAddress)) {
			throw new SocketException("Cannot bind to this type of address: " + addr.getClass());
		}
		final UNIXSocketAddress socketAddress = (UNIXSocketAddress) addr;
		socketFile = socketAddress.getSocketFile();
		NativeUnixSocket.connect(socketFile, fd);
		this.address = socketAddress.getAddress();
		this.port = socketAddress.getPort();
		this.localport = 0;
		this.connected = true;
	}

	@Override
	protected void create(boolean stream) throws IOException {
	}

	@Override
	protected InputStream getInputStream() throws IOException {
		if (!connected && !bound) {
			throw new IOException("Not connected/not bound");
		}
		return in;
	}

	@Override
	protected OutputStream getOutputStream() throws IOException {
		if (!connected && !bound) {
			throw new IOException("Not connected/not bound");
		}
		return out;
	}

	@Override
	protected void listen(int backlog) throws IOException {
		NativeUnixSocket.listen(fd, backlog);
	}

	@Override
	protected void sendUrgentData(int data) throws IOException {
		NativeUnixSocket.write(fd, new byte[] { (byte) (data & 0xFF) }, 0, 1);
	}

	private final class AFUNIXInputStream extends InputStream {
		private boolean streamClosed = false;

		@Override
		public int read(byte[] buf, int off, int len) throws IOException {
			if (streamClosed) {
				throw new IOException("This InputStream has already been closed.");
			}
			if (len == 0) {
				return 0;
			}
			int maxRead = buf.length - off;
			if (len > maxRead) {
				len = maxRead;
			}
			try {
				return NativeUnixSocket.read(fd, buf, off, len);
			}
			catch (final IOException e) {
				IOException ioe = new IOException(e.getMessage() + " at " + UNIXSocketImpl.this.toString());
				ExceptionUtil.initCauseEL(ioe, e);
				throw ioe;
			}
		}

		@Override
		public int read() throws IOException {
			final byte[] buf1 = new byte[1];
			final int numRead = read(buf1, 0, 1);
			if (numRead <= 0) {
				return -1;
			}
			else {
				return buf1[0] & 0xFF;
			}
		}

		@Override
		public void close() throws IOException {
			if (streamClosed) {
				return;
			}
			streamClosed = true;
			if (fd.valid()) {
				NativeUnixSocket.shutdown(fd, SHUT_RD);
			}

			closedInputStream = true;
			checkClose();
		}

		@Override
		public int available() throws IOException {
			final int av = NativeUnixSocket.available(fd);
			return av;
		}
	}

	private final class AFUNIXOutputStream extends OutputStream {
		private boolean streamClosed = false;

		@Override
		public void write(int oneByte) throws IOException {
			final byte[] buf1 = new byte[] { (byte) oneByte };
			write(buf1, 0, 1);
		}

		@Override
		public void write(byte[] buf, int off, int len) throws IOException {
			if (streamClosed) {
				throw new UNIXSocketException("This OutputStream has already been closed.");
			}
			if (len > buf.length - off) {
				throw new IndexOutOfBoundsException();
			}
			try {
				while (len > 0 && !Thread.interrupted()) {
					final int written = NativeUnixSocket.write(fd, buf, off, len);
					if (written == -1) {
						throw new IOException("Unspecific error while writing");
					}
					len -= written;
					off += written;
				}
			}
			catch (final IOException e) {
				IOException ioe = new IOException(e.getMessage() + " at " + UNIXSocketImpl.this.toString());
				ExceptionUtil.initCauseEL(ioe, e);
				throw ioe;
			}
		}

		@Override
		public void close() throws IOException {
			if (streamClosed) {
				return;
			}
			streamClosed = true;
			if (fd.valid()) {
				NativeUnixSocket.shutdown(fd, SHUT_WR);
			}
			closedOutputStream = true;
			checkClose();
		}
	}

	@Override
	public String toString() {
		return super.toString() + "[fd=" + fd + "; file=" + this.socketFile + "; connected=" + connected + "; bound=" + bound + "]";
	}

	private static int expectInteger(Object value) throws SocketException {
		try {
			return (Integer) value;
		}
		catch (final ClassCastException e) {
			throw new UNIXSocketException("Unsupported value: " + value, e);
		}
		catch (final NullPointerException e) {
			throw new UNIXSocketException("Value must not be null", e);
		}
	}

	private static int expectBoolean(Object value) throws SocketException {
		try {
			return ((Boolean) value).booleanValue() ? 1 : 0;
		}
		catch (final ClassCastException e) {
			throw new UNIXSocketException("Unsupported value: " + value, e);
		}
		catch (final NullPointerException e) {
			throw new UNIXSocketException("Value must not be null", e);
		}
	}

	@Override
	public Object getOption(int optID) throws SocketException {
		try {
			switch (optID) {
			case SocketOptions.SO_KEEPALIVE:
			case SocketOptions.TCP_NODELAY:
				return NativeUnixSocket.getSocketOptionInt(fd, optID) != 0 ? true : false;
			case SocketOptions.SO_LINGER:
			case SocketOptions.SO_TIMEOUT:
			case SocketOptions.SO_RCVBUF:
			case SocketOptions.SO_SNDBUF:
				return NativeUnixSocket.getSocketOptionInt(fd, optID);
			default:
				throw new UNIXSocketException("Unsupported option: " + optID);
			}
		}
		catch (final UNIXSocketException e) {
			throw e;
		}
		catch (final Exception e) {
			throw new UNIXSocketException("Error while getting option", e);
		}
	}

	@Override
	public void setOption(int optID, Object value) throws SocketException {
		try {
			switch (optID) {
			case SocketOptions.SO_LINGER:

				if (value instanceof Boolean) {
					final boolean b = (Boolean) value;
					if (b) {
						throw new SocketException("Only accepting Boolean.FALSE here");
					}
					NativeUnixSocket.setSocketOptionInt(fd, optID, -1);
					return;
				}
				NativeUnixSocket.setSocketOptionInt(fd, optID, expectInteger(value));
				return;
			case SocketOptions.SO_RCVBUF:
			case SocketOptions.SO_SNDBUF:
			case SocketOptions.SO_TIMEOUT:
				NativeUnixSocket.setSocketOptionInt(fd, optID, expectInteger(value));
				return;
			case SocketOptions.SO_KEEPALIVE:
			case SocketOptions.TCP_NODELAY:
				NativeUnixSocket.setSocketOptionInt(fd, optID, expectBoolean(value));
				return;
			default:
				throw new UNIXSocketException("Unsupported option: " + optID);
			}
		}
		catch (final UNIXSocketException e) {
			throw e;
		}
		catch (final Exception e) {
			throw new UNIXSocketException("Error while setting option", e);
		}
	}

	@Override
	protected void shutdownInput() throws IOException {
		if (!closed && fd.valid()) {
			NativeUnixSocket.shutdown(fd, SHUT_RD);
		}
	}

	@Override
	protected void shutdownOutput() throws IOException {
		if (!closed && fd.valid()) {
			NativeUnixSocket.shutdown(fd, SHUT_WR);
		}
	}

	/**
	 * Changes the behavior to be somewhat lenient with respect to the specification.
	 * 
	 * In particular, we ignore calls to {@link Socket#getTcpNoDelay()} and
	 * {@link Socket#setTcpNoDelay(boolean)}.
	 */
	static class Lenient extends UNIXSocketImpl {
		Lenient() {
			super();
		}

		@Override
		public void setOption(int optID, Object value) throws SocketException {
			try {
				super.setOption(optID, value);
			}
			catch (SocketException e) {
				switch (optID) {
				case SocketOptions.TCP_NODELAY:
					return;
				default:
					throw e;
				}
			}
		}

		@Override
		public Object getOption(int optID) throws SocketException {
			try {
				return super.getOption(optID);
			}
			catch (SocketException e) {
				switch (optID) {
				case SocketOptions.TCP_NODELAY:
				case SocketOptions.SO_KEEPALIVE:
					return false;
				default:
					throw e;
				}
			}
		}
	}
}
